Chapter 13: 13. Object-Oriented Programming with Python
From book Python Programming (Problem solving, Packages and Libraries) published by McGraw Hill Education (India) Private limited.
By:
Note the following:-
Markdown and code cells (extension .ipynb) and then downloaded as html. If someone wants to "modify" or "extend' this document, you may ask for the original .ipynb file by sending me an e-mail at:- 999.anuraggupta@gmail.com13.2.7. Concept of operator overloading
See Page 316 of the book
There is a concept in object-oriented programming which is called operator overloading.
It is best explained by an example:
Suppose you have two variables $x$ and $y$ and you write a statement: $z = x + y$. Here $x$ and $y$ are the operands and $+$ is the operator. Suppose $x = 3$ and $y = 4$, then both are of type int and the + operator will add these two int values and assign the result to $z$, i.e., 7.
But suppose you have $x =\ ’3’$ and $y = \ '4'$, then what? Should the result be ‘addition’ of numbers to give $7$? Or should it be joining (concatenation) of the two digits (which here are strings) leading to an answer of $34$?
Operator for addition i.e. ($+$) ‘sees’ the operands on both sides of the operator (i.e., $x$ and $y$ here) and decides accordingly what to do. If both operands are integers it ‘adds’ them. If both operands are strings it ‘concatenates’ them. So operator ‘$+$’ behaves differently depending on the operands. This is operator overloading.
Do however note that if the two operands are of different types for the $+$ operator, it will first try to “cast” one of the operands to the “type” of other and then do the operation. But if casting is not possible then it will generate error. It will be clear from the following code:
# ---ON IDLE---
# Multiple assignments on same line permitted as follows
>>> x,y = 3,4# Due to this assignment of 3 and 4, both x and y are of type int
>>> z = x+y #x and y are ‘added’ like number in maths
>>>print(z)
7
>>> x,y = '3','4' # However here both x and y are of type string (str)
>>> z = x+y # Here x and y are not ‘added’ like integers but rather concatenated
>>>print(z)
34
>>>
13.2.8. Short note on function overloading
Function overloading is not present in Python. Why? Because function overloading is also called “compile time polymorphism” and since Python is not a compiled language but an “interpreted” language, there is no function overloading in Python. However, “function overloading” is available in compiled languages like Java and C++.
It is a very simple concept:
Following code shows it but read this after you have understood how to write class definition in Python:
class A:
def f1(self): # First definition of f1()
print("1st implementation")
def f1(self, var = '2nd implementation'): # 2nd implementation of f1()
print(var)
a = A() # Create object of class A
a.f1() # The second definition of f1() will overwrite the first always
13.2.9. Creating a simple class and simple objects
Note: As per the Python style guide, class names should always begin with a capital letter.
In Python, the syntax for writing code of a class in pseudo-code is:
class ClassName:
<statement-1>
.
.
.
<statement-N>
You can create a Person class as follows:
class Person:
name = "XXX"
age = 0
sex = "Male"
# Create 2 objects p1 and p2 of Person class
p1 = Person()
p2 = Person()
print('p1 name->',p1.name, 'p2 age->',p2.age)
p1.name = "Anil"
p2.age = 20
print('p1 name->',p1.name, 'p2 age->',p2.age)
13.3. OOP concepts related specifically to Python
13.3.1. A class which has an __init__() method
There is a serious problem with the Person class created above. All objects of type Person have same name ‘xxx’, age 0 and sex ‘Male’.
Suppose you wanted to be able to create objects of type Person with name, age and sex as per your own choice, then how can you do this?
The answer to this problem in programming languages lies in the concept of a ‘constructor’. All modern object-oriented languages like C++ and Java have ‘constructors’ to initialize the instance of an object as per parameters supplied by the creator of the object.
What is a constructor to a class? When you create an object of a class type, you may at times want to initialize it with certain values. For example, you may want to create an object say p1 of class type Person, with name, sex and age supplied by the creator of the object. To do this, you must define a constructor in the class
In Python, this is done by the __init__() method.
__init__() method has double under scores on both sides. self’. When you create an object by using as shown above p1 = Person(), then you can access the properties of the object using dot notation like p1.name. The use of ‘self’ as inside a class definition is shown in the following code:
class Person:
def __init__(self, name, sex, age):
self.myName = name
self.mySex = sex
self.myAge = age
# Create objects p1 and p2
p1 = Person("Sunil Kumar", 'Male', 19)
p2 = Person("Anita", "Female", 18)
print("p1 is ", p1.myName,'sex ', p1.mySex, 'age ',p1.myAge)
print("p2 is ", p2.myName,'sex ', p2.mySex, 'age ',p2.myAge)
In many programming examples, often the local variables passed to the __init__() functions are same as the attributes of the object.
In the script given below, the attributes of an object of Person class are name, sex and age which is same as parameters passed to the __init__() function.
You should not get confused by a statement like: self.name = name. It simply means that the parameter name passed to the constructor__init__() has been assigned to the name attribute of the Person class. So there are two names: The first name is the parameter passed to the __init__() function and the second name is the attribute of an object of Person class. The first name will exist only within the __init__() function, but the second name will be there as long as object of type Person exists.
For the script below, see Page 320 of the book
class Person:
def __init__(self, name, sex, age):# pass name, sex, age parameters to init
self.name = name # LHS variable name is attribute of Person class
self.sex = sex
self.age = age
p1 = Person('Sunil Kumar', 'Male', 19)
print('p1 name-> ',p1.name)
13.3.2. A class which has attributes, __init__() and also default values for __init__().
It is also possible to provide default values to an object created by providing these values to the __init__() function.
Why do you need default values? One reason could be that most of the objects being created are having a particular value for an attribute.
For example suppose the above Person class was being used to create objects who were mostly 20 years. Then you could give the default value of 20 to the age attribute. This is shown in the following code:
class Person:
def __init__(self, name, sex, age = 20):
self.name = name
self.sex = sex
self.age = age
anju = Person('Anju Kumari', 'Female')
print('Anju age-> ',anju.age) # default 20 taken for age attribute of anju object
sunita = Person('Sunita Kumari', 'Female', 25) #3rd param given.Override default
print('Sunita Age-> ',sunita.age) # Default value of 20 overridden by 25
Note that the default arguments can be provided to the class constructor in two ways. In the first way, it is provided by position. Here you can provide the default arguments to the parameters starting from the right. In the above example, you provided default of 20 to age which is the right most argument to the __init__(). You could have provided default to the next argument from right also, i.e., sex as follows:
class Person:
def __init__(self, name, sex = 'Female', age = 20):
self.name = name
self.sex = sex
self.age = age
anita = Person('Anita Kumari')
print('Anita is-> ',anita.sex)
However, you cannot assign a default parameter to the second parameter of the __init__() while not providing a default to the third. This is shown as follows (There will be an error):
class Person:
def __init__(self, name, sex = 'Female', age):
self.name = name
self.sex = sex
self.age = age
p = Person('Sunita',50)
However, you can change the order of providing the arguments to the class constructors by specifying the attribute name.
Hence, the following code is possible:
For script below, see Page 322 of the book
class Person:
def __init__(self, name, sex = 'Female', age = 20):
self.name = name
self.sex = sex
self.age = age
amit = Person(sex = 'Male', age = 22, name = 'Amit Kumar') # attrib order changed
print('Name-> ',amit.name,'Sex-> ',amit.sex, 'Age-> ',amit.age)
13.3.3. A class which has attributes as well as member functions or class methods
So far you have studied classes which have attributes and also an __init__() function. However, class in Python can also have member functions. This will become clear from an example.
Suppose you want to have two member functions of the Person class. The first will set() the city of residence of the object of Person type and the second will get() the city of residence of the Person type. This is shown as follows:
class Person:
def __init__(self, name, sex = 'Female', age = 20):
self.name = name
self.sex = sex
self.age = age
self.lang = 'Hindi'
def setL(self, lang):#Method of Person class. First parameter must be self
self.lang = lang
def getL(self): # Another method of Person class
return self.lang
# Create objects
radha = Person("Radha Kumari")
print('Before setting language-> ',radha.getL())
radha.setL("English") # Set language to English
print('After setting language-> ',radha.getL())
13.3.4. Concept of instance methods (or methods applicable to objects), static methods and class methods
As pointed out earlier, in Python, there are classes and instances of these classes called objects.
There may be a scenario where you are not interested in “instance” variables but “class variables”.
In the Person class used above, suppose the programmer wanted to know the number of objects of type Person created. This number is not relevant to a particular object, but rather to a class as a whole.
The class variables are accessed using the class name rather than the object name. This is similar to a car factory, where the individual car objects may not be interested in knowing how many cars are produced, but the factory manager might want to know the number of cars produced.
Consider the following code:
class Person:
count = 0
def __init__(self, name, sex = 'Female', age = 20):
Person.count = Person.count +1
self.name = name
self.sex = sex
self.age = age
def numP(): # No self parameter in method numP() because it is staticmethod
print('count->',Person.count)
numP = staticmethod(numP) # old method of defining a static method
# create 3 objects of Person class ie anita, sunita and sunil
anita = Person('Anita')
sunita = Person('Sunita')
sunil = Person('Sunil', 'Male', 19)
Person.numP() # Call to static method numP() of Person Class
13.3.5. Function decorator @staticmethod
There is another way of telling the Python interpreter that a particular method is a static method. This way is by using function decorators.
A function decorator is placed just before the def statement. It starts with a @ symbol.
For example the function decorator for declaring a method as static in Python is @staticmethod.
Note that @staticmethod function is just a function “defined inside a class”, nothing more.
You can “call” a static method without first “instantiating, i.e., creating an instance” of the class.
Hence, the example given above (In which you have used numP = staticmethod(numP), could be also written using @staticmethod as follows:
class Person:
count = 0
def __init__(self, name, sex = 'Female', age = 20):
Person.count = Person.count +1
self.name = name
self.sex = sex
self.age = age
@staticmethod #Function decorator
def numP(): # No self parameter in numP() because it is staticmethod
print('count->',Person.count)
# create 3 objects of Person class ie anita, sunita and sunil
anita = Person('Anita')
sunita = Person('Sunita')
sunil = Person('Sunil', 'Male', 19)
Person.numP() # Call to static method numP() of Person Class
Another example will clarify the need for static methods. Suppose
The following code shows this:
class Products:
tax_rate = 0
def __init__(self, name = ''):
self.name = name
print('tax rate for object->', Products.tax_rate)
@staticmethod
def add_interest(rate_increase = 0):
Products.tax_rate = Products.tax_rate + rate_increase
print('new tax rate->', Products.tax_rate)
# Increase tax_rate
Products.add_interest(0.5)
Products.add_interest(0.5)
# Create object of Products class
pen = Products('pen')
13.3.6. Data hiding, mangling, pseudo-private member variables in a class
Python supports the concept of name “mangling”. The concept is very simple. Inside a class you may use a variable name which is also being used in another class. How do you differentiate between these two similar variables? Especially in case of inheritance where you are deriving child classes from parent classes, there is a chance that the variable names being used by parent or child classes may clash. Python provides a work around to this problem by using the concept of “name mangling”.
The “mangling” algorithm works as follows:
__var or __var_ but not _var or _var__ or __var___ClassName.__varName where ClassName is the class to which the variable belongs and __varName is the name of the variable name. This will be clear from the following example:
class C1:
x = 'cat'# Normal variable
__var = 'Dog'# Variable __var is now mangled
c = C1()
print('Normal variable-> ', c.x)
print('Mangled Variable-> ', c._C1__var) #OK
13.4. Some common “built in” attributes and methods of a Python mo`dules and classes
13.4.1. __name__
See Page 329 of the book
Classes in Python have attributes. In Python, every module (which is nothing but a .py file) also has an “attribute” called __name__. So __name__ is a “module attribute” and not a class/ object attribute. You know that every module (i.e., every .py file) in Python can either be imported or executed. Following about __name__ are relevant:
__name__ attribute will have a “value of ” __main__. __name__ attribute of the module will have a value equal to the “name of the module”. __name__ is an attribute of a module and not of any class. __name__ attribute.__name__ attribute. The following code on Jupyter clarifies the concept. In this example, the __name__ of module being executed is __main__. But the __name__ attribute of the “re” module, which is being imported is re not __main__
import re
print('__name__ of main module->',__name__)
# The re module is imported, so its __name__ is re
print('__name__ of imported re module->',re.__name__)
13.4.2. __module__
Just like the __name__ attribute, __module__ is also an attribute of a Python module and not of a class/ object.
For example, consider the Python module re for regular expressions. You have studied earlier that this module has a method search().
So if you import this search() function and later on want to know, from where it has been imported, you can do so using the __module__ attribute.
This is shown in the following code on Jupyter
# Import the search() function from the re module
from re import search
# Use __module__ attribute to get name of the module of the function
print('search() belongs to module->',search.__module__)
13.4.3. __dict__
The__dict__ attribute “automatically provided” attributes of both classes and objects in form of a dictionary and can be accessed by using:
ClassName.__dict__ (for getting the key:value pair attributes of a class) andObject_name.__dict__ (for getting the key:value pair attributes of an object) Again note that the __dict__ attribute can be used on a
This is clear from the following code:
class A:
x = 'X'# x is a Class variable
y = 'Y'# y is also a class variable
def __init__(self):
self.w = 'W'
print('__dict__ of Class A',A.__dict__)# A.__dict__ gives namespace of Class A
a = A() # a is an object of type A
print('__dict__ of object a',a.__dict__) #a.__dict__ gives namespace of object a
13.4.4. __doc__
Another common automatically provided attribute to a class/ object/ function is __doc__.
Python provides a way of accessing documentation which is attached to modules, classes and functions.
The following script shows this:-
# ---ON IDLE---
>>>import math #math is a module which is part of Python library
>>> math.__doc__
'This module is always available. It provides access to the\nmathematical functions defined by the C standard.'
>>>
You may create your own Class A which has a method f1() and also create an object of Class A as follows:
class A:
''' This is docstring of class A'''
def f1(self):
''' This is doc string of function f1'''
a = A() #Create an object of class A
# .__doc__ can be called on class name or on object or function name
print('Doc string of class A->',A.__doc__) #Call __doc__ on class
print('Doc string of object a->', a.__doc__) # call __doc__ on object
print('Doc string of function f1-> ', A.f1.__doc__) # call __doc__ on function
13.4.5.__bases__
In Python, classes also have a __bases__ attribute. (Note __bases__ is an attribute of a class and not of a module).
The __bases__ attribute gives a tuple of references to the super classes (i.e., all the classes from which this class has inherited).
Inheritance is dealt later in the book. However, all the inbuilt data types like int, str, etc. are classes in Python and their base classes can be got using the __bases__ attribute as follows:
# ---ON IDLE---
# ON IDLE
>>> int.__bases__ #int, str, tuple-> All are class whose base class is object
(<class'object'>,)
>>> str.__bases__
(<class'object'>,)
>>> tuple.__bases__
(<class'object'>,)
>>>class A:
pass
>>> A.__bases__
(<class'object'>,)
Further note that in Python, if no class is specified in the class definition, the class will by default inherit from the object. However, it is possible to explicitly inherit from the object as shown in the following code:
class A: # class A is implicitly derived from class object
pass
class B(object): # class B is explicitly derived from class object
pass
print("Base class of class A-> ", A.__bases__)
print("Base class of class B-> ", B.__bases__)
13.4.6.__del__()
__del__() is the destructor method in Python.
__init__() and a __del__() class method. __del__() simply prints ‘Car destroyed’ to tell that it was called. The following script shows this:-
class Car:
def __init__(self, name = 'No name'):
self.name = name
print('Car created ->', self.name)
def __del__(self):
print('Car destroyed-> ', self.name)
#... Create object with 1 reference
maruti = Car('Maruti') # Object maruti of type Car created
maruti = 10#maruti does not refer to object of type Car anymore-> destroyed
However if you create two references to the same object and then remove one of the two references, then the Garbage collector will not call the __del__() method. The GC waits for the second reference to be removed and then calls the GC as shown in the following code:
class Car:
def __init__(self, name = 'No name'):
self.name = name
print('Car created ->', self.name)
def __del__(self):
print('Car destroyed-> ', self.name)
# Create a Car object
myCar = Car('Ford')
# Create an alias to Car object
aliasCar = myCar
# Destroy first car object
del myCar # Will not call __del__()
print('myCar destroyed but __del__() not called so far')
# Destroy alias to Car object
del aliasCar # Will call __del__()
13.4.7. some_object.__str__()
See Page 335 of the book
some_object.__str__(), some_object represents some Python object. __str__() is the “string representation” of the given object.
object.__str__(self) __str__() member method from inside the class. str(object) function, where object is the object whose string representation is desired. Note that the Python built-in function str(some_object), in turn calls some_object.__str__().Dog class which does not have any __str__() method. This is as follows:
class Dog:
def __init__(self, legs =4, color = 'Black'):
self.legs = legs
self.color = color
tommy = Dog()#tommy is an instance of Dog class
print(str(tommy))# Same as print(tommy)
print(tommy) # String representation of object tommy will be printed
Now you may create the same Dog class but with a __str__() method. This is shown in the following code:
class Dog:
def __init__(self, legs =4, color = 'Black'):
self.legs = legs
self.color = color
def __str__(self):
return 'Object of Dog class'#String representation of object of class Dog
tommy = Dog() # tommy is an instance of Dog class
print(str(tommy)) # Same as print(tommy)
print(tommy) # String representation of object tommy will be printed
13.7. Exercise
See Page 338 of the book
2. Create a class Animal. This class should have a class variable animal_type and it should also have an object variable animal_type. This is to say that both class variable and object (i.e., instance of class) should have same variable name.
Solution:-
class Animal:
animal_type = 'class animal' # class variable
def __init__(self, animal_type = 'object animal'):
self.animal_type = animal_type # object variable
# animal1 is instance ie object of Animal class
animal1 = Animal()
print('class animal_type->', Animal.animal_type)
print('object animal_type->', animal1.animal_type)
c. Find and write the output of the following Python code:
class Emp:
def __init__(self, code, nm): # Constructor
self.Code = code
self.Name = nm
def manip(self):
self.Code = self.Code + 10
self.Name = 'Karan'
def show(self, line):
print(self.Code, self.Name, line)
s = Emp(25, 'Mamta')
s.show(1)
s.show(2)
print(s.Code + len(s.Name))
13.8. Beyond text book
See Page 339 of the book
a. In the chapter the topic string representation of an object of an object was covered. It is possible to get the string representation of an object by implementing the __str__() method in the class definition. But Python provides another such method called ,the __repr__() method. The difference between the two is subtle (slight) but important.
__repr__() is called by inbuilt function repr(Object_name) and gives the “official string representation”, while __str__() when called by inbuilt function str() gives the “informal string representation”.__repr__() method but does not implement the __str__(), method, then calling the inbuilt str() function will give the string of the __repr__() method of the class.
This can be best understood by the following example:# Dog class does not have __str__() method
# But Dog class has a __repr__() method
class Dog:
def __init__(self, legs =4, color = 'Black'):
self.legs = legs
self.color = color
def __repr__(self):
return 'Object of Dog class (From __repr__())'
tommy = Dog()#tommy is an instance of Dog class
# There is no __str__() method in Dog class
# So the str() function will use the __repr__() method of Dog class
print(str(tommy))# Same as print(tommy)
print(tommy) # String representation of object tommy will be printed
b. Decorators in Python:
This topic is not covered in the book
In the chapter there was a brief discussion on one decorator namely @staticmethod. But “decorators” were not explained in details.
Note that there are two types of decorators in Python, i.e., “in-built” and “user-defined”. To understand the concept, you may create “your own decorator”.
Have a look at the following code:
# decorate in 2 different ways
def some_func():
print('An ordinary function')
# A function which takes another function name as parameter
def my_decorator_func(a_func):
def nested_func():
print('Decorate->')
# Now call the function to be decorated
a_func()
return nested_func
# Method 1 of decoration
decorated_func = my_decorator_func(some_func)
decorated_func()
# Method 2 of decoration
@my_decorator_func
def another_func():
print('Another ordinary but decorated function')
another_func()
c. The garbage collector (gc) module
This topic is not covered in the book
- Using the assignment operator.
- Passing the object as an argument (for example, passing an object as an argument to a function or a class).
sys.getrefcount() function of the sys module. You can check out the docstring of getrefcount() on Jupyter as follows:
# ---ON IDLE---
import sys
?sys.getrefcount()
The following script shows how the getrefcount() function is used (There will be an error):
import sys
L = [1, 2, 3]
print('ref counts of L->', sys.getrefcount(L)) # refcount is 2
L2 = L
print('ref counts of L->', sys.getrefcount(L)) # refcount is 3
del L2
print('ref counts of L->', sys.getrefcount(L)) # refcount is 2
del L
print('ref counts of L->', sys.getrefcount(L)) # Error L doesnt exist any more
While the “reference count” garbage collector cannot be controlled by a programmer, it is possible to control the generational gc. Python provides a module called gc for this purpose and through this module one can get “access” to the generational garbage collector.
You can check the details of all the functions of this module gc on Jupyter as shown in the following code:
# ---ON IDLE---
import gc
?gc
The output (Truncated and modified) is
# ---ON IDLE---
# OUTPUT (Truncated and modified)
Type: module
String form: <module 'gc' (built-in)>
Docstring:
This module provides access to the garbage collector for reference cycles.
enable() -- Enable automatic garbage collection.
disable() -- Disable automatic garbage collection.
isenabled() -- Returns true if automatic collection is enabled.
collect() -- Do a full collection right now.
get_count() -- Return the current collection counts.
get_stats() -- Return list of dictionaries containing per-generation stats.
set_debug() -- Set debugging flags.
get_debug() -- Get debugging flags.
set_threshold() -- Set the collection thresholds.
get_threshold() -- Return the current the collection thresholds.
get_objects() -- Return a list of all objects tracked by the collector.
is_tracked() -- Returns true if a given object is tracked.
get_referrers() -- Return the list of objects that refer to an object.
get_referents() -- Return the list of objects that an object refers to.
From above you can see that the gc module can be used in a number of ways to modify the behavior of the generational garbage collector. You can even stop the generational gc using disable() or start it using enable() or run it even when the “threshold” has not been reached by using gc.collect().
The following code shows how you can get the threshold values for the three generations, get the number of objects in each generation and use the collect() function:
import gc
print('default threshold values->', gc.get_threshold())
print('current objects in each generation->', gc.get_count())
print('Use collect()->', gc.collect())
print('After collect() objects in each generation->', gc.get_count())
d. Using the ctypes “foreign library” to detect “cyclic references” in Python.
This topic is not covered in the book
If you delete an object yet if it is still present in memory, i.e., it is not collected by the garbage collector, then how does one know about this?
Following is a script which shows that cyclic references lead to objects existing in the memory even after destruction.
The script works as follows:
id() inbuilt function. This address will be used to locate the object with cyclic reference in the memory.RefCounter is sub-classed from the Structure base class of the ctypes module.RefClass has an attribute _fields_. You must be careful in defining this attribute because it has to be a list of 2 tuples. The first item in the tuple is the “name” of the field and you can chose any arbitrary name. Here the name “ref_cnt” has been chosen. But the second item in the tuple must be a valid ctypes data. Here ctypes.c_long has been chosen.Structure base class (from which the class RefCounter was sub-classed) has a method from_address(some_address). This method provides “a C instance at the specified address”. You can check out this method by using the following on Jupyter:# ---ON IDLE---
import ctypes
?ctypes.Structure.from_address
The output (Truncated and modified) is
# ---ON IDLE---
# OUTPUT (Truncated and modified)
Docstring:
C.from_address(integer) -> C instance
access a C instance at the specified address
Type: builtin_function_or_method
import ctypes
class RefCounter(ctypes.Structure):
# ctypes.Structure is an abstract class, so
# the _fields_ attribute must be specified
_fields_ = [('ref_cnt', ctypes.c_long)]
# Create a list
L = [1, 2, 3]
# Append the list to itself. So cyclic reference created
L.append(L)
# Get address of L
L_address = id(L)
print(L_address)
# Get memory location of the C object
c_obj = RefCounter.from_address(L_address)
print(c_obj)
# Use the ref_cnt created earlier using _fields_ attribute of RefCounter class
ref_counts = c_obj.ref_cnt
print('references to L before destruction->',ref_counts)
del L
# Now you cannot access L from the script
ref_counts = c_obj.ref_cnt
print('references to L after destruction->',ref_counts)